package com.aphrodite.ui.widgets.barcode;

import java.util.Vector;

import com.aphrodite.io.CharBuffer;
import com.aphrodite.util.CharUtil;

/**
 * This is a concrete implementation of the Code 128 barcode. It fully supports
 * all three available character sets (A, B and C), and also fully supports code
 * shifts and set changes on-the-fly, providing an automatic optimisation mode.
 * 
 */
class Code128Barcode extends LinearBarcode {
	/** Character set A flag */
	public static final int A = 0;
	/** Character set B flag */
	public static final int B = 1;
	protected static final int[] BUF_SIZES = { 1, 1, 2 };
	/** Character set c flag */
	public static final int C = 2;

	/** Code set change from current to A character */
	public static final String CHANGE_TO_A = "\311";
	/** Code set change from current to B character */
	public static final String CHANGE_TO_B = "\310";
	/** Code set change from current to c character */
	public static final String CHANGE_TO_C = "\307";
	/** FNC1 start character */
	public static final String FNC_1 = "\312";

	/** Auto character set flag */
	public static final int O = 3;

	protected static final Module QUIET_SECTION = new BlankModule(10);
	/** Code shift character */
	public static final String SHIFT = "\306";

	private static final Module START_A = new Module(new int[] { 2, 1, 1, 4, 1, 2 });
	private static final Module START_B = new Module(new int[] { 2, 1, 1, 2, 1, 4 });

	private static final Module START_C = new Module(new int[] { 2, 1, 1, 2, 3, 2 });

	protected static final int[] START_INDICES = { 103, 104, 105 };
	private static final Module[] START = { START_A, START_B, START_C };
	protected static final Module STOP = new Module(new int[] { 2, 3, 3, 1, 1, 1, 2 });

	private CharBuffer buf;
	private Module checkDigit;
	private C128Accumulator index;
	protected int mode;
	private boolean optimising;
	private boolean shifted;
	private boolean shiftNext;
	protected int startIndex;
	private int startingMode;
	private C128Accumulator sum;

	// /**
	// * Create a new Code 128 barcode using character set B.
	// *
	// * @param data
	// * The data to encode
	// * @throws BarcodeException
	// * If the data to be encoded is invalid
	// */
	// Code128Barcode(String data) throws BarcodeException {
	// this(data, O, 50);
	// }

	/**
	 * Creates a new Coded 128 barcode with the specified data and the specified
	 * character set.
	 * 
	 * @param data
	 *            The data to encode
	 * @param mode
	 *            The character set to use for encoding
	 * @throws BarcodeException
	 *             If the data to be encoded is invalid
	 */
	Code128Barcode(String data, int mode, int height) throws BarcodeException {
		super(data, height);
		if (mode == O) {
			optimising = true;
			this.mode = B;
		} else {
			optimising = false;
			this.mode = mode;
		}
		this.startingMode = this.mode;
		this.shiftNext = false;
		this.shifted = false;
		this.startIndex = START_INDICES[this.mode];
	}

	// /**
	// * Returns the current character set being used in this barcode.
	// *
	// * @return The flag indicating the current character set
	// */
	// public int getCharacterSet() {
	// return startingMode;
	// }

	/**
	 * Calculates the check sum digit for the barcode.
	 * 
	 * @return The check sum digit
	 */
	public Module calculateChecksum() {
		if (checkDigit == null) {
			encodeData();
		}
		return checkDigit;
	}

	/**
	 * Calculates the minimum allowed barcode height for the barcode. The height
	 * must be at least .15 times the length of the symbol (excluding quiet
	 * zones) and .25 inches (whichever is larger).
	 * 
	 * @param resolution
	 *            The output resolution (for calculating the width)
	 * @return The minimum height
	 */
	protected int calculateMinimumBarHeight(int resolution) {
		// The height of the bars must be at least .15 times the symbol's length
		// or .25 inches,
		// whichever is larger
		double point25Inches = resolution * 0.25;
		// TODO: Need to get rid of this and do it in the output class
		return (int) Math.max((0.15 * getSymbolWidth(resolution)), point25Inches);
	}

	private void checkCodeChange(Module module) {
		if (module instanceof C128ModuleFactory.C128CodeChangeMod) {
			mode = ((C128ModuleFactory.C128CodeChangeMod) module).getCode();
			buf = new CharBuffer(BUF_SIZES[mode]);
		}
	}

	private void checkShift(Module module) {
		if (module instanceof C128ModuleFactory.C128ShiftMod) {
			mode = shiftMode();
			shiftNext = true;
		} else if (shiftNext) {
			shifted = true;
		}
	}

	public void cleanUp() {
	// TODO Auto-generated method stub

	}

	private void clearShift() {
		if (shifted) {
			shifted = false;
			shiftNext = false;
			mode = shiftMode();
		}
	}

	private boolean containsShiftOrChange(String data) {
		return ((data.indexOf(CHANGE_TO_A) >= 0) || (data.indexOf(CHANGE_TO_B) >= 0)
				|| (data.indexOf(CHANGE_TO_C) >= 0) || (data.indexOf(SHIFT) >= 0));
	}

	private double convertToMillimetres(double barWidth, int resolution) {
		// 25.4 mm in 1 inch
		double pixelsPerMM = resolution / 25.4;
		return barWidth / pixelsPerMM;
	}

	private boolean digitGroupEndIsNext(int index, String chars) {
		if (index == chars.length() - 1) {
			return true;
		}
		char c1 = chars.charAt(index);
		char c2 = chars.charAt(index + 1);
		return ((Character.isDigit(c1) && (!Character.isDigit(c2))) || (!Character.isDigit(c1)));
	}

	private boolean digitGroupIsNext(int index, String chars) {
		char c1 = chars.charAt(index);
		char c2 = chars.charAt(index + 1);
		char c3 = chars.charAt(index + 2);
		char c4 = chars.charAt(index + 3);
		return (Character.isDigit(c1) && Character.isDigit(c2) && Character.isDigit(c3) && Character.isDigit(c4));
	}

	private void encode(Vector modules, String data) {
		Module module = C128ModuleFactory.getModule(data, mode);
		updateCheckSum(data);
		checkShift(module);
		checkCodeChange(module);
		modules.addElement(module);
		clearShift();
	}

	/**
	 * Encodes the data of the barcode into bars.
	 * 
	 * @return The encoded bar data
	 */
	public Module[] encodeData() {
		// We are calculating the check digit as we encode - this will ensure
		// that it is
		// calculated correctly, even with code changes to char set C
		sum = new C128Accumulator(startIndex);
		Vector modules = new Vector();
		buf = new CharBuffer(BUF_SIZES[mode]);
		index = new C128Accumulator(1);
		padDataToEvenLength();

		for (int i = 0; i < data.length(); i++) {
			char c = data.charAt(i);
			if (optimising && startingMode == B) {
				if (i + 1 < data.length() && isControl(c) && mode != A) {
					if (mode == B) {
						encode(modules, SHIFT);
					} else {
						encode(modules, CHANGE_TO_A);
					}
				} else if (i + 3 < data.length() && digitGroupIsNext(i, data) && mode != C) {
					encode(modules, CHANGE_TO_C);
				} else if (i + 1 <= data.length() && digitGroupEndIsNext(i, data) && mode == C && buf.size() != 1) {
					encode(modules, CHANGE_TO_B);
				}
			}

			if (isShiftOrCode(c)) {
				encode(modules, String.valueOf(c));
				buf.clear();
			} else {
				buf.addChar(c);
				if (buf.isFull()) {
					encode(modules, buf.toString());
					buf.clear();
				}
			}
		}

		checkDigit = C128ModuleFactory.getModuleForIndex(sum.getValue() % 103, mode);
		mode = startingMode;
		Module[] ret = new Module[modules.size()];
		modules.copyInto(ret);
		return ret;
	}

	/**
	 * Returns the text label to be displayed underneath the barcode/
	 * 
	 * @return The text label for the barcode
	 */
	public String getLabel() {
		if (label != null) {
			return label;
		} else {
			return data;
		}
	}

	/**
	 * Returns the post amble for the barcode. This is the combination of the
	 * stop character anda quiet section.
	 * 
	 * @return The post amble
	 */
	protected Module getPostAmble() {
		CompositeModule module = new CompositeModule();
		module.add(STOP);
		if (drawingQuietSection) {
			module.add(QUIET_SECTION);
		}
		return module;
	}

	/**
	 * Returns the pre-amble for the barcode. This is a combination of a quiet
	 * section and the start character for the character set that the barcode
	 * was constructed with.
	 * 
	 * @return The pre-amble
	 */
	protected Module getPreAmble() {
		CompositeModule module = new CompositeModule();
		if (drawingQuietSection) {
			module.add(QUIET_SECTION);
		}
		module.add(START[mode]);
		return module;
	}

	/**
	 * Returns the width of the encoded symbol portion of the barcode in pixels
	 * for the given resolution.
	 * 
	 * @param resolution
	 *            The resolution to calculate the width for
	 * @return The width of the encoded portion of the barcode
	 */
	protected double getSymbolWidth(int resolution) {
		// L = (11C + 35)X (alphanumeric) L = (5.5C + 35)X (numeric only using
		// Code C)
		// where
		// L = length of symbol (not counting quiet zone)
		// C = number of data characters, code characters and shift characters
		// (do not include start, stop or checksum. They are automatically added
		// in.)
		// X = X-dimension
		double barWidthMM = convertToMillimetres(width, resolution);
		double multiplier = 11;
		if (startingMode == C) {
			multiplier = 5.5;
		}
		return (multiplier * data.length() + 35) * barWidthMM;
	}

	private boolean isControl(char c) {
		return CharUtil.isISOControl(c);
	}

	private boolean isShiftOrCode(char c) {
		String s = String.valueOf(c);
		return (s.equals(SHIFT) || s.equals(CHANGE_TO_A) || s.equals(CHANGE_TO_B) || s.equals(CHANGE_TO_C) || s
				.equals(FNC_1));
	}

	/**
	 * Pads the data to be encoded to an even length by prepending "0"
	 * characters. This is only valid for pure character set C barcodes.
	 */
	private void padDataToEvenLength() {
		// Only for Code C
		if (startingMode == C && data.length() % 2 != 0 && !containsShiftOrChange(data)) {
			data = '0' + data;
		}
	}

	private int shiftMode() {
		if (mode == A) {
			return B;
		} else {
			return A;
		}
	}

	private void updateCheckSum(String data) {
		int code = C128ModuleFactory.getIndex(data, mode);
		sum.add(code * index.getValue());
		index.increment();
	}
}
